package cn.zhxu.data.snack3;

import cn.zhxu.data.Array;
import cn.zhxu.data.DataConvertor;
import cn.zhxu.data.Mapper;
import org.noear.snack.ONode;
import org.noear.snack.core.Feature;
import org.noear.snack.core.Options;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.List;

/**
 * @author noear
 * @since 2022.07.07
 * */
public class Snack3DataConvertor implements DataConvertor {

    private TemporalCodec<LocalDate> localDateCodec = new TemporalCodec<>("yyyy-MM-dd");
    private TemporalCodec<LocalDateTime> localDateTimeCodec = new TemporalCodec<>("yyyy-MM-dd HH:mm");
    private TemporalCodec<LocalTime> localTimeCodec = new TemporalCodec<>("HH:mm");

    @Override
    public Mapper toMapper(InputStream in, Charset charset) {
        return toMapper(toString(in, charset));
    }

    @Override
    public Mapper toMapper(String in) {
        return new Snack3Mapper(ONode.load(in, decoderOptions()));
    }

    @Override
    public Array toArray(InputStream in, Charset charset) {
        return toArray(toString(in, charset));
    }

    @Override
    public Array toArray(String in) {
        return new Snack3Array(ONode.load(in, decoderOptions()));
    }

    @Override
    public byte[] serialize(Object object, Charset charset) {
        return serialize(object, false).getBytes(charset);
    }

    @Override
    public byte[] serialize(Object object, Charset charset, boolean pretty) {
        return serialize(object, pretty).getBytes(charset);
    }

    @Override
    public String serialize(Object object, boolean pretty) {
        if (object instanceof Mapper) {
			object = ((Mapper) object).toMap();
		}
        if (object instanceof Array) {
            object = ((Array) object).toList();
        }
        Options options = new Options();
        if (pretty) {
            options.add(Feature.PrettyFormat);
        }
        options.addEncoder(LocalDate.class, localDateCodec);
        options.addEncoder(LocalDateTime.class, localDateTimeCodec);
        options.addEncoder(LocalTime.class, localTimeCodec);
        return ONode.stringify(object, options);
    }

    @Override
    public <T> T toBean(Type type, InputStream in, Charset charset) {
        try {
            return ONode.load(toString(in, charset), decoderOptions()).toObject(type);
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
    }

    @Override
    public <T> T toBean(Type type, String in) {
        return ONode.load(in, decoderOptions()).toObject(type);
    }

    private Options decoderOptions() {
        Options options = new Options();
        // 当前版本的 Snack3 仍存在以下问题：
        //   当字段上加了 @ONodeAttr.format 后，在序列化时，会直接使用 @ONodeAttr.format，不会走自定义的 Encoder，
        //   但是在 反序列化时，它不会直接使用 @ONodeAttr.format，而是走了自定义的 Decoder
        //   所以，这里添加了 LocalTime 的 Decoder，在反序列化时，会忽略 @ONodeAttr.format
        //   等待 Snack3 升级即可
        options.addDecoder(LocalTime.class, localTimeCodec);
        return options;
    }

    @Override
    public <T> List<T> toList(Class<T> type, InputStream in, Charset charset) {
        return toList(type, toString(in, charset));
    }

    @Override
    public <T> List<T> toList(Class<T> type, String in) {
        return ONode.load(in, decoderOptions()).toObjectList(type);
    }

    protected String toString(InputStream in, Charset charset) {
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        byte[] buff = new byte[512];
        try {
            int len;
            while ((len = in.read(buff)) > 0) {
                output.write(buff, 0, len);
            }
        } catch (IOException e) {
            throw new IllegalStateException(e);
        }
        try {
            return output.toString(charset.name());
        } catch (UnsupportedEncodingException e) {
            throw new IllegalStateException(e);
        }
    }

    public TemporalCodec<LocalDate> getLocalDateCodec() {
        return localDateCodec;
    }

    public void setLocalDateCodec(TemporalCodec<LocalDate> localDateCodec) {
        this.localDateCodec = localDateCodec;
    }

    public TemporalCodec<LocalDateTime> getLocalDateTimeCodec() {
        return localDateTimeCodec;
    }

    public void setLocalDateTimeCodec(TemporalCodec<LocalDateTime> localDateTimeCodec) {
        this.localDateTimeCodec = localDateTimeCodec;
    }

    public TemporalCodec<LocalTime> getLocalTimeCodec() {
        return localTimeCodec;
    }

    public void setLocalTimeCodec(TemporalCodec<LocalTime> localTimeCodec) {
        this.localTimeCodec = localTimeCodec;
    }

}
